<?php
/**
 * PSA functions.
 * 
 * LICENSE:
 * 
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with This library. If not, see <{@link http://www.gnu.org/licenses/}>.
 *
 * @link http://code.google.com/p/phpstartapp/
 * @author Bojan Mauser <bmauser@gmail.com>
 * @copyright Bojan Mauser 2009
 * @package psa
 * @version $Id: functions.php 464 2009-03-24 00:30:12Z bmauser $
 */


/**
 * Deletes user or group. Removes all data about the user or group from database.
 * Do not use this function directly. Use psa_group_del() and psa_user_del() wrapper functions.
 * 
 * @param int|array|string $id id of the user|group or array with user|group ids. 'all' for 
 * deleting all users|groups.
 * @param int $user_or_group what to delete. 1-user, 2-group
 * @return int 0 for failure, 1 for success, -1 user|group (or more users|groups) not exists
 * @see psa_group_del()
 * @see psa_user_del()
 * @ignore
 */
function psa_del_user_group($id,$user_or_group){

	if($id && $user_or_group){
			
		// global config array
		global $PSA_CFG;

		// global database object
		global $psa_database;
			
		// if $id is not array make it array for foreach loop
		if(!is_array($id))
			$id = array($id);
			
		// flag if some query fail in the foreach loop
		$failed = 0;
		
		// work with users
		if($user_or_group == 1){	
			$table = $PSA_CFG['database']['table']['user'];
			$name_column = 'psa_username';
			$plugins_to_run_before = 'Psa_Plugin_Before_User_Delete';
			$plugins_to_run_after = 'Psa_Plugin_After_User_Delete';
		}
		// work with groups
		else if($user_or_group == 2){
			$table = $PSA_CFG['database']['table']['group'];
			$name_column = 'psa_name';
			$plugins_to_run_before = 'Psa_Plugin_Before_Group_Delete';
			$plugins_to_run_after = 'Psa_Plugin_After_Group_Delete';
		}

		// delete users or groups
		foreach ($id as $id_key => &$id_value){

			// delete all users or groups
			if($id_value == 'all')
				$sql = "DELETE FROM {$table}";
			// delete user or groups by id
			else if(is_int($id_value)){
				$sql = "DELETE FROM {$table} WHERE psa_id = '$id_value'";
			}
			// delete user or groups by name
			else{
				$sql = "DELETE FROM {$table} WHERE $name_column = " . $psa_database->escape($id_value);
			}
			
			// run Psa_Plugin_Before_[user/group]_Delete plugins
			psa_run_plugins(array($plugins_to_run_before => array('psa_main' => array($id_value))),'by_type','no_unregistered_warning');
				
			// run query against the database
			$psa_database->query($sql);

			// if no rows affected
			if($psa_database->affected_rows() <= 0){
				$failed = 1;
				$log_data['message']  = 'Unable to delete ' . (($user_or_group == 1) ? 'user' : 'group') . ". Maybe does not exists.";
			}
			// run Psa_Plugin_After_[user/group]_Delete plugins
			else{
				psa_run_plugins(array($plugins_to_run_after => array('psa_main' => array($id_value))),'by_type','no_unregistered_warning');
				$log_data['message']  = (($user_or_group == 1) ? 'User' : 'Group') . ' deleted';
			}
			
			// logging
			if($PSA_CFG['logging']['max_log_level'] >= 2){
				// parameters for Psa_Logger::log() method
				$log_data['function'] = __FUNCTION__;
				$log_data['level']    = 2;
				
				if($user_or_group == 1){
					if(is_int($id_value))
						$log_data['user_id'] = $id_value;
					else
						$log_data['username'] = $id_value;
				}
				else if($user_or_group == 2){
					if(is_int($id_value))
						$log_data['group_id'] = $id_value;
					else
						$log_data['groupname'] = $id_value;
				}
				
				Psa_Logger::get_instance()->log($log_data);
			}
		}
			
		if($failed)
			return -1;
		else
			return 1;			
	}
	return 0;
}


/**
 * Deletes user. Removes all data about the user from database.
 * If you want to delete user by id be sure to pass integer as argument (or array of integers) 
 * and use cast operator <kbd>(int)</kbd>
 * {@internal This is a wrapper function for psa_del_user_group()}}
 * 
 * @param int|array|string $user ID or username of the user or array with user 
 * ids or usernames. <kbd>all</kbd> to delete all users.
 * @return int 0 for failure, 1 for success, -1 user (or more users) not exists
 * {@internal @see psa_del_user_group()}}
 */
function psa_user_del($user){
	return psa_del_user_group($user,1);
}


/**
 * Deletes group. Removes all data about the group from database.
 * If you want to delete group by id be sure to pass integer as argument (or array of integers) 
 * and use cast operator <kbd>(int)</kbd>
 * {@internal This is a wrapper function for psa_del_user_group()}}
 * 
 * @param int|array|string $group ID or name of the group or array with group ids or group names. 
 * <kbd>all</kbd> to delete all groups.
 * @return int 0 for failure, 1 for success, -1 group (or more groups) not exists
 * {@internal @see psa_del_user_group()}}
 */
function psa_group_del($group){
	return psa_del_user_group($group,2);
}


/**
 * Calls <kbd>debug_backtrace()</kbd> PHP function and returns formatted backtrace string.
 * It is used for logs.
 * Example of returned string:<pre>
 * query() called at [/WEBROOT/bf/psa/scripts/Psa_User.class.php:391]
 * add_remove_group() called at [/WEBROOT/bf/psa/scripts/Psa_User.class.php:327]
 * add_group() called at [/WEBROOT/bf/index.php:17]
 * </pre>
 * 
 * @return string
 */
function psa_get_backtrace(){

	// global config array
	global $PSA_CFG;

	// backtrace info
	$backtrace = debug_backtrace();

	$backtrace_str = '';

	foreach ($backtrace as $backtrace_key => &$backtrace_value){

		// skip the first one, since it's always this function
		if ($backtrace_key == 0)
		continue;

		$backtrace_str .= $backtrace_value['function'] . '()' . ' called at [' .  str_replace ("\\", '/', @$backtrace_value['file']) . ':' . @$backtrace_value['line'] . "]\r\n";
	}

	return $backtrace_str;
}


/**
 * Calls plugin methods. Executes (runs) plugins.
 * This function includes plugin files, makes new instances of plugin objects named like plugin class 
 * and calls member methods. 
 * It can call plugin's methods in two different ways depending on second argument.
 * If the second argument is false (default) plugins are called <i>by id</i> (plugin id is same
 * as plugin class name) and in this case first argument should be array with elements
 * like in this example:
 * <code>
 * // $run is array that will be argument for psa_run_plugins() function
 * // Structure should be: $run['plugin_id']['plugin_method'] = array('method_argument1','method_argument2',...);  
 * $run['Article']['load'] = array(14);
 * $run['News']['open'] = array();
 * psa_run_plugins($run);
 * </code>
 * This example calls method <kbd>load</kbd> with one argument (14) from the <kbd>Article</kbd> class 
 * (plugin) instanced as object named <kbd>Article</kbd> and method <kbd>open</kbd> with no arguments 
 * from the <kbd>News</kbd> class instanced as object named <kbd>News</kbd>.
 * 
 * If the second argument is true plugins are called <i>by type</i> and in this case first argument 
 * should be an array with elements like in example below:
 * <code>
 * // $run is array that will be argument for psa_run_plugins() function
 * // Structure should be: $run['plugin_type']['plugin_method'] = array('method_argument1','method_argument2',...);  
 * $run['Psa_Plugin_Before_User_Delete']['psa_main'] = array(177);
 * psa_run_plugins($run,'by_type');
 * </code>
 * This example calls method <kbd>psa_main</kbd> with one argument (177) from all registered plugins
 * of type <kbd>Psa_Plugin_Before_User_Delete</kbd>.
 * 
 * This function will throw {@link Psa_Exception} on error.
 * 
 * @param array $run_details array with details what to run
 * @param bool $by_type if true plugins will be called <i>by type</i> otherwise will be called <i>by id</i>
 * @param bool $disable_unregistered_exception exception that plugin is not registered will not be thrown. 
 * This parameter can be set to true when plugin doesn't have to be registered like 
 * {@link Psa_Plugin_After_User_Create}.
 * @return int 1:success all plugins executed, -1:success, but not with all plugins because some are 
 * probably not registered. -1 can be returned only when third argument is set to true otherwise 
 * exception will be thrown.
 * Some are probably not registered. See log for details. 
 * @see Psa_Plugin::register()
 * @throws Psa_Exception
 */
function psa_run_plugins($run_details, $by_type = false, $disable_unregistered_exception = false){
	
	// global config array
	global $PSA_CFG;
	
	// request id for profile logging
	static $request_id = null;
	
	// plugin object
	$psa_plugins = Psa_Plugin::get_instance();

	
	// if router plugin is called make exception and allow executing without any registered plugin because
	// router is called first and should be called without reading data about plugins from database.
	if(!$psa_plugins->plugins_data && isset($run_details['Router']['psa_main'])){
		
		if(isset($PSA_CFG['router_location']))
			$plugins_data_temp['all_plugins']['Router']['file'] = $PSA_CFG['folders']['basedir'] . '/' . $PSA_CFG['router_location'] . '/' . 'Router.php';
		else
			$plugins_data_temp['all_plugins']['Router']['file'] = $PSA_CFG['folders']['basedir'] . '/' . $PSA_CFG['folders']['plugins'][0] . '/' . 'Router.php';
			
		$plugins_data_temp['all_plugins']['Router']['type'] = 'Psa_Plugin_Router';
		$psa_plugins__plugins_data = &$plugins_data_temp;
	}
	else
		// set reference to $psa_plugins->plugins_data array
		$psa_plugins__plugins_data = &$psa_plugins->plugins_data;
	
	// die if plugin are not registered or not retrieved from session or database
	if(!$psa_plugins__plugins_data or !is_array($psa_plugins__plugins_data['all_plugins'])){
		include_once 'Psa_Fatal_Error_Exception.class.php';
		$msg = 'No plugins to run. Plugins are not registered or data not retrieved from session or database';
		throw new Psa_Fatal_Error_Exception($msg, $msg, 16);
	}
	
	
	// exit if $run_details is not array
	if(!is_array($run_details)){
		include_once 'Psa_Exception.class.php';
		throw new Psa_Exception('Invalid first argument for psa_run_plugins() function. Not array.', 11);
	}
	
	
	// call all plugins of same type
	if($by_type){
		
		$plugin_type = key($run_details);
		
		// if plugin is registered
		if(isset($psa_plugins__plugins_data[$plugin_type]))
			$plugins_data = &$psa_plugins__plugins_data[$plugin_type];
		else{	
			if(!$disable_unregistered_exception){
				include_once 'Psa_Exception.class.php';
				throw new Psa_Exception("Trying to run unregistered plugin type of $plugin_type. Try to register plugins and delete session (cookies) if you store registered plugin data in session.", 12);
			}
			else
				return 0;
		}
	}
	// call plugins by id (plugin class name)
	else{
		$plugins_data = &$run_details;
	}
	
	
	// set default return value
	$return = 1;
	
	
	// $plugins_data is array with data used to instance classes and run methods (class name, method name, file name)
	foreach ($plugins_data as $plugins_data_key => &$plugins_data_value){
		
		// if call all plugins of same type
		if($by_type){
			$include_file = $plugins_data_value['file'];
			$plugin_class = $plugins_data_key;
			$plugin_methods_data = &$run_details[$plugin_type]; // array of method names and arguments
		}
		// if we call plugins by id
		else{
			
			// if plugin is not registered
			if(!isset($psa_plugins__plugins_data['all_plugins'][$plugins_data_key])){
				
				if(!$disable_unregistered_exception){
					include_once 'Psa_Exception.class.php';
					throw new Psa_Exception("Trying to run unregistered plugin $plugins_data_key. Try to register plugins and delete session (cookies) if you store registered plugin data in session.", 13);
				}
				else{
					$return = -1;
					
					continue;
				}
			}
			
			$include_file = &$psa_plugins__plugins_data['all_plugins'][$plugins_data_key]['file'];
			$plugin_class = &$plugins_data_key;
			$plugin_methods_data = &$plugins_data_value; // array of method names and arguments
		}
			
		// include plugin file
		require_once $include_file;
		
		// check if plugin object is already instanced
		if(!isset($$plugin_class)){ //!is_object (@$$plugin_class)
			
			// TODO: check if we have arguments for __construct method 

			// make instance of plugin object
			$$plugin_class = new $plugin_class;
		}
		
		// call plugin object methods
		if(is_array($plugin_methods_data)){
			
			foreach ($plugin_methods_data as $plugin_method_name => $plugin_method_args){
				
				// check if method exists
				if(method_exists($$plugin_class, $plugin_method_name)){
				
					// I couldn't use call_user_func_array() here because it is suitable only for static methods.
					// So i use PHP reflection api to invoke method with array of arguments.
					// Also here can eval() be used.
					// Simplest and the fastest solution would be: $$plugin_class_name->$plugin_method_name($plugin_method_args);
					// but then invoking method must be written to accept one parameter which is array with values that
					// normally would be separate method parameters.	
					
					// create an instance of the Reflection_Method class
					$invoke_method = new ReflectionMethod($plugin_class, $plugin_method_name);
					
					// collect informations about request
					if($PSA_CFG['develop_mode']){
						$temp_Registry_ref = Psa_Registry::get_instance();
						$temp_Registry_ref->psa_request_info['executed_plugins'][] = array($plugin_class, $plugin_method_name, $plugin_method_args, $psa_plugins__plugins_data['all_plugins'][$plugin_class]['type'], $psa_plugins__plugins_data['all_plugins'][$plugin_class]['file'], print_r(Psa_Result::get_instance(),true),'res_before');
					}
					
					// If profile log is enabled
					if($PSA_CFG['profile_log'] && !isset($$plugin_class->psa_no_profile_log)){
						$time_start = microtime(true);
						if(!$request_id)
							$request_id = uniqid('', true);
					}
					
					// call plugin method with arguments
					$invoke_method->invokeArgs($$plugin_class, $plugin_method_args);
					
					// If profile log is enabled
					if($PSA_CFG['profile_log'] && !isset($$plugin_class->psa_no_profile_log)){
						$log_data['method'] = $plugin_class . '->' . $plugin_method_name;
						$log_data['total_time'] = microtime(true) - $time_start;
						$log_data['method_arguments'] = print_r($plugin_method_args,true);
						$log_data['request_id'] = $request_id;
						Psa_Profile_Logger::get_instance()->log($log_data);
					}
					
					// collect informations about request - I want Psa_Result after executing plugin method
					if($PSA_CFG['develop_mode']){
						$temp_executed_plugins_num = count($temp_Registry_ref->psa_request_info['executed_plugins'])-1;
						$temp_Registry_ref->psa_request_info['executed_plugins'][$temp_executed_plugins_num][5] = print_r(Psa_Result::get_instance(),true);
						$temp_Registry_ref->psa_request_info['executed_plugins'][$temp_executed_plugins_num][6] = 1;
					}
				}
				else{
					if(!$disable_unregistered_exception){
						include_once 'Psa_Exception.class.php';
						throw new Psa_Exception("Trying to run unexisting plugin method $plugin_class::$plugin_method_name", 14);
					}
					else{
						$return = -1;
					}
				}
			}
		}
	}
	
	return $return;
}


/**
 * This function is registered with PHP spl_autoload_register() function as __autoload() implementation 
 * used to auto include files with PSA plugin classes or your plugins.
 * Thus you don't have to put <kbd>include()</kbd> call to include file with <i>Psa_Plugin_</i> 
 * abstract class in every your plugin file before you extend some <i>Psa_Plugin_</i> class.
 * 
 * Also if you want to call a method from some another plugin class, or extend some plugin class, 
 * you don't have to include its file.
 * 
 * For example, you can just write:
 * <code>
 * <?php
 * class Default_Controller extends Psa_Plugin_Controller{
 * 	function psa_main(){
 * 		// ... something here
 * 	}
 * }
 * ?>
 * </code>
 */
function psa_autoload($class_name){
	
	// global config array
	global $PSA_CFG;
	
	$psa_plugins = Psa_Plugin::get_instance();
	
	// first try to include class of PSA plugin type
	if(substr($class_name, 0, 11) == 'Psa_Plugin_'){
		
		// if plugin data is available
		if($psa_plugins->plugins_data && array_key_exists($class_name, $psa_plugins->plugins_data['all_psa_plugin_types'])){
			include_once $psa_plugins->plugins_data['all_psa_plugin_types'][$class_name];
			return;
		}
		
		foreach ($PSA_CFG['folders']['plugin_types'] as $plugin_type_folder) {
			$include_file = $PSA_CFG['folders']['basedir'] . '/' . $plugin_type_folder . '/' . $class_name . '.class.php';
			if(is_file($include_file)){
				include_once $include_file;
				return;
			}
		}
	}
	
	// then try to include registered plugins
	if($psa_plugins->plugins_data && array_key_exists($class_name, $psa_plugins->plugins_data['all_plugins'])){
		include_once $psa_plugins->plugins_data['all_plugins'][$class_name]['file'];
		return;
	}	
}


/**
 * I use this function instead of echo() or print_r() functions during development.
 * 
 * @param mixed $str
 * @param bool $return_only
 * @ignore 
 */
function prs($str, $return_only = false){
	$return = '<pre>' . htmlspecialchars (print_r($str, TRUE)) . '</pre>';
	
	if($return_only)
		return $return;
	
	echo $return;
	flush ();
}


/**
 * Returns array with parts of sql query used to store and retrive data from custom database table columns. 
 * This is helper function used in Psa_User and Psa_Group objects.
 * 
 * @param $type int 1-for UPDATE query, 2-for INSERT query
 * @param $data array data for the query
 * @param object $ignore_unseted if property does not exists inside $ignore_unseted object do not include it result arrays
 * @param array $changed_properties array with indexes same as those in $data array that you want to include in results
 * @return array
 * @ignore 
 */
function psa_orm_make_query_parts($type, $data){
	
	$return['insert_query_columns'] = '';
	$return['insert_query_values'] = '';
	$return['update_query_set'] = '';
	$return['values'] = array();
	
	if(sizeof($data)){
		
		// global database object
		global $psa_database;
		
		foreach ($data as $data_key => $data_value){
			
			// UPDATE query
			if($type == 1)	
				$return['update_query_set'] .= ', ' . $data_key . " = ?";	
			
			// INSERT query
			else if($type == 2){
				$return['insert_query_columns'] .= ", $data_key";
				$return['insert_query_values'] .= ', ?';
			}
			
			$return['values'][] = $data_value;
		}
	}
	
	return $return;
}


/**
 * Checks if user is a member of a group.
 * It simply tries to find a matching row in <i>psa_user_in_group</i> database table.
 * 
 * @param int $user user id
 * @param int $group group id
 * @return int 1-user is a member of the group, otherwise 0
 */
function psa_is_user_in_group($user, $group){
	
	global $PSA_CFG, $psa_database;
	
	$user = (int)$user;
	$group = (int)$group;
	
	if($user && $group){
		
		$sql = "SELECT * FROM {$PSA_CFG['database']['table']['user_in_group']} WHERE user_id = '$user' AND group_id = '$group'";
		$psa_database->query($sql);
		
		$row = $psa_database->fetch_row();
		
		if($row['user_id'])
			return 1;
	}
	
	return 0;
}


/**
 * Returns HTML code with request information. 
 * Request information can be nicely shown on the web page you develop by including
 * html code returned by this function.
 * You can put HTML returned somewhere on the web pages and see information like 
 * all plugins that are called, {@link Psa_Result} object, $_GET, $_POST, $_SESSION, 
 * plugin methods arguments... That can be very useful while developing and debugging.
 * See example {@tutorial psa_features.pkg#requestinfo here}.
 * 
 * The template file this function expects is <i>psa_request_info.tpl</i> and it is
 * by default in PSA main directory. Develop mode must be on to enable collecting request 
 * information. You can enable it through main settings in {@link config.php} file. 
 * Actually request information is saved in {@link Psa_Registry} object in 
 * <kbd>psa_request_info</kbd> member array and this function is reading it form there.
 * 
 * @param string $tpl_folder folder where is <i>psa_request_info.tpl</i> template file. 
 * If not set PSA main folder is used. 
 * @return bool|string rendered template - HTML code, false if develop mode is not on.
 */
function psa_request_info_html($tpl_folder = null){
	
	global $PSA_CFG;
	
	// return if develop mode is disabled
	if(!$PSA_CFG['develop_mode'])
		return false;
	
	// include Psa_Dully class
	include_once $PSA_CFG['folders']['basedir'] . '/scripts/Psa_Dully.class.php';
	
	// make new Psa_Dully template engine object
	$dully = new Psa_Dully;
	
	// folder with templates
	if(!$tpl_folder)
		$tpl_folder = $PSA_CFG['folders']['basedir'];
	$dully->template_dir = $tpl_folder;
	
	$registry = Psa_Registry::get_instance();
	
	$objects[1]['menu_text'] = '$psa_result';
	$objects[1]['content'] = Psa_Result::get_instance();
	
	$objects[2]['menu_text'] = '$_GET';
	$objects[2]['content'] = @$registry->psa_request_info['globals']['_GET'];
	
	$objects[3]['menu_text'] = '$_POST';
	$objects[3]['content'] = @$registry->psa_request_info['globals']['_POST'];
	
	$objects[4]['menu_text'] = '$_SESSION';
	$objects[4]['content'] = @$registry->psa_request_info['globals']['_SESSION'];
	
	$objects[5]['menu_text'] = '$_COOKIE';
	$objects[5]['content'] = @$registry->psa_request_info['globals']['_COOKIE'];
	
	$objects[6]['menu_text'] = '$_SERVER';
	$objects[6]['content'] = @$registry->psa_request_info['globals']['_SERVER'];
	
	$objects[7]['menu_text'] = '$PSA_CFG';
	$objects[7]['content'] = $PSA_CFG;
	
	// hide database password
	$objects[7]['content']['pdo']['password'] = 'xxxxx';
	
	$objects[8]['menu_text'] = 'Psa_Plugin';
	$objects[8]['content'] = Psa_Plugin::get_instance();
	
	$dully->assign('executed_plugins',@$registry->psa_request_info['executed_plugins']);
	$dully->assign('objects',$objects);
	$dully->assign('log',@$registry->psa_request_info['log']['psa_default']);
	$dully->assign('log_profile',@$registry->psa_request_info['log']['psa_profile']);
	
	return $dully->fetch('psa_request_info.tpl');
}


/**
 * Adds given path to PHP include_path configuration directive.
 * It can be useful when you work with some externals libraries like Zend Framework.
 * 
 * @param string $path path to add to include path
 * @return string|bool returns the old include_path on success or FALSE on failure. 
 * @see http://www.php.net/manual/en/ini.core.php#ini.include-path
 */
function psa_add_include_path($path){
	
	if($path){	        
		return set_include_path(get_include_path() . PATH_SEPARATOR . $path);
	}
	
	return false;
}

